import {
  PainterOptions,
  IPaintElement,
  IPaintImage,
  TDrawElement,
  IPaintText,
  IPaintBlock,
  IPaintMultipleLine,
  IClipOptions,
  IPaintCircle,
  IPainterShadow,
  IGetCuttingBoxParams,
  ICuttingBox,
  IPaintBorder,
} from "./types";

const double = (n : number) : number => Number(n.toFixed(2));
const int = (n : number) : number => Math.floor(n);

export default class Painter {
  private canvasId : string;
  private ctx : UniApp.CanvasContext;
  private width : number;
  private height : number;
  private prevElement : IPaintElement = { x: 0, y: 0, width: 0, height: 0 };

  constructor(
    canvasId : string,
    canvas : any,
    options : PainterOptions
  ) {
    this.canvasId = canvasId;
    const { height, width } = options;
    this.width = width;
    this.height = height;
    const ctx = uni.createCanvasContext(canvasId, canvas);
    this.ctx = ctx;
    console.log("Painter init:", { width: this.width, height: this.height })
  }
  measureTextWidth(txt : string) {
    const { ctx } = this;
    return txt && ctx.measureText(txt).width || ''
  }
  drawBorder(element : TDrawElement<IPaintBorder>) : Promise<void> {
    return new Promise((resolve) => {
      const { ctx } = this;
      const {
        color = '#000',
        borderWidth = 1,
        x,
        y,
        width,
        height,
        dontRecord,
      } = typeof element === "function" ? element(this.prevElement) : element;

      ctx.save();
      ctx.rect(x, y, width, height)
      ctx.setLineWidth(borderWidth)
      ctx.setStrokeStyle(color)
      ctx.stroke()

      ctx.restore();

      if (!dontRecord) {
        this.prevElement = { x, y, width, height };
      }
      this.ctx.draw(true, resolve);
    })
  }

  drawBlock(element : TDrawElement<IPaintBlock>) : Promise<void> {
    return new Promise((resolve) => {
      const { ctx } = this;
      const {
        bgColor = "#FFF",
        x,
        y,
        width,
        height,
        borderRadius,
        dontRecord,
      } = typeof element === "function" ? element(this.prevElement) : element;

      const _drawBlock = () => {
        ctx.setFillStyle(bgColor);
        ctx.fillRect(x, y, width, height);
      };
      ctx.save();
      if (borderRadius) {
        this.clip({ x, y, width, height, borderRadius, next: _drawBlock });
      } else {
        _drawBlock();
      }
      ctx.restore();

      if (!dontRecord) {
        this.prevElement = { x, y, width, height };
      }
      this.ctx.draw(true, resolve);
    });
  }

  drawCircle(element : TDrawElement<IPaintCircle>) : Promise<void> {
    return new Promise((resolve) => {
      const { ctx } = this;
      const { x, y, size, dontRecord, bgColor } =
        typeof element === "function" ? element(this.prevElement) : element;
      ctx.save();
      ctx.arc(x + size / 2, y + size / 2, size / 2, 0, 2 * Math.PI);
      if (bgColor) {
        ctx.fillStyle = bgColor;
        ctx.fill();
      }

      ctx.restore();
      if (!dontRecord) {
        this.prevElement = { x, y, width: size, height: size };
      }
      this.ctx.draw(true, resolve);
    });
  }

  private getCuttingBox({
    imageHeight,
    imageWidth,
    width,
    height,
  } : IGetCuttingBoxParams) : ICuttingBox {
    let cuttingBox : ICuttingBox = { x: 0, y: 0, width: 0, height: 0 };

    const imgPer = double(imageWidth / imageHeight);
    const boxPer = double(width / height);

    if (imgPer > boxPer) {
      const _width = int((imageHeight / height) * width);
      const _x = int((imageWidth - _width) / 2);
      cuttingBox = { x: _x, y: 0, width: _width, height: imageHeight };
    } else if (imgPer < boxPer) {
      const _height = int((imageWidth / width) * height);
      const _y = int((imageHeight - _height) / 2);
      cuttingBox = { x: 0, y: _y, width: imageWidth, height: _height };
    } else {
      cuttingBox = { x: 0, y: 0, width: imageWidth, height: imageHeight };
    }
    return cuttingBox;
  }

  drawImage(element : TDrawElement<IPaintImage>) : Promise<void> {
    return new Promise((resolve) => {
      const { ctx } = this;
      const {
        image,
        x,
        y,
        width,
        height,
        borderRadius,
        imageWidth,
        imageHeight,
        dontRecord,
        shadow,
        mode = "aspectFill",
      } = typeof element === "function" ? element(this.prevElement) : element;

      const _drawImg = () => {
        if (imageWidth && imageHeight && mode === "aspectFill") {
          const cuttingBox = this.getCuttingBox({
            imageHeight,
            imageWidth,
            width,
            height,
          });
          ctx.drawImage(
            image,
            cuttingBox.x,
            cuttingBox.y,
            cuttingBox.width,
            cuttingBox.height,
            x,
            y,
            width,
            height
          );
        } else {
          ctx.drawImage(image, x, y, width, height);
        }
      };

      ctx.save();
      if (shadow) {
        this.shadow(shadow, { x, y, width, height });
      }
      if (borderRadius) {
        this.clip({
          x,
          y,
          width,
          height,
          borderRadius,
          next: _drawImg,
        });
      } else {
        _drawImg();
      }
      ctx.restore();
      if (!dontRecord) {
        this.prevElement = { x, y, width, height };
      }
      this.ctx.draw(true, resolve);
    });
  }

  drawText(element : TDrawElement<IPaintText>) : Promise<void> {
    return new Promise((resolve) => {
      const { ctx } = this;
      const e =
        typeof element === "function" ? element(this.prevElement) : element;
      const {
        x,
        y,
        text,
        align = "left",
        color = "#000000",
        size = 14,
        baseLine = "top",
        dontRecord,
        fontWeight = "normal",
		 fontF='sans-serif'
      } = e;
      const lineHeight = e.lineHeight || size;
      const offset = Math.round((lineHeight - size) / 2);
      ctx.save();
      ctx.font = `${fontWeight} ${size}px  ${fontF}`;
      ctx.setFillStyle(color);
      ctx.setTextAlign(align);
      ctx.setTextBaseline(baseLine);
      ctx.fillText(text, x, y + offset);
      ctx.restore();
      const textWidth = ctx.measureText(text).width;

      if (!dontRecord) {
        this.prevElement = {
          x,
          y,
          width: textWidth,
          height: lineHeight,
        };
      }
      this.ctx.draw(true, resolve);
    });
  }

  async drawMultipleLine(element : TDrawElement<IPaintMultipleLine>) {
    const { ctx } = this;
    const e =
      typeof element === "function" ? element(this.prevElement) : element;
    const {
      x,
      y,
      text,
      align = "left",
      color = "#000000",
      size = 14,
      baseLine = "top",
      line,
      maxWidth,
      dontRecord,
      fontWeight,
	  fontF='sans-serif'
    } = e;
    const lineHeight = e.lineHeight || size;
    // const offset = Math.round((lineHeight - size) / 2);
    let _tmp = "";
    let _curLine = 1;
    const end = "...";
    ctx.font = `${size}px ${fontF}`;
    const len = text.length;

    const _drawText = async (t: string) => {
      await this.drawText({
        x,
        y: y + (_curLine - 1) * lineHeight,
        color,
        align,
        size,
        baseLine,
        dontRecord: true,
        text: t,
        lineHeight,
        fontWeight,
		fontF
      });
    };

    for (let i = 0; i < len; i++) {
      const s = text[i];
      const nextText = _tmp + s;
      const nextWidth = ctx.measureText(nextText).width;
      // 最后一行需要 ...
      if (_curLine === line) {
        const lastWidth = ctx.measureText(nextText + end).width;
        if (lastWidth > maxWidth) {
          await _drawText(_tmp + end);
          break;
        } else {
          _tmp += s;
          if (i === len - 1) {
            await _drawText(_tmp);
          }
        }
      } else {
        // 非最后一行
        if (nextWidth > maxWidth) {
          await _drawText(_tmp);
          _tmp = s;
          _curLine++;
        } else {
          _tmp += s;
          if (i === len - 1) {
            await _drawText(_tmp);
          }
        }
      }
    }

    if (!dontRecord) {
      this.prevElement = {
        x,
        y,
        height: _curLine * lineHeight,
        width: maxWidth,
      };
    }
  }

  private shadow(
    options : IPainterShadow,
    block : Omit<IPaintElement, "dontRecord">
  ) {
    const { ctx } = this;
    const {
      shadowOffsetX = 0,
      shadowOffsetY = 0,
      shadowColor = "rgba(0, 0, 0, .5)",
      shadowBlur = 0,
    } = options;
    const { x, y, width, height } = block;
    ctx.shadowOffsetX = shadowOffsetX;
    ctx.shadowOffsetY = shadowOffsetY;
    // @ts-ignore
    ctx.shadowColor = shadowColor;
    ctx.shadowBlur = shadowBlur;
    ctx.setFillStyle("rgba(0, 0, 0, 0)");
    ctx.fillRect(x, y, width, height);
  }

  private clip(options : IClipOptions) {
    const { ctx } = this;
    const { x, y, width, height, borderRadius, next } = options;
    // 规定剪裁区域
    ctx.beginPath();
    ctx.setFillStyle("#fff");
    ctx.arc(
      x + borderRadius,
      y + borderRadius,
      borderRadius,
      Math.PI,
      Math.PI * 1.5
    ); //画圆角矩形 start
    ctx.moveTo(x + borderRadius, y);
    ctx.lineTo(x + width - borderRadius, y);
    ctx.lineTo(x + width, y + borderRadius);
    ctx.arc(
      x + width - borderRadius,
      y + borderRadius,
      borderRadius,
      Math.PI * 1.5,
      Math.PI * 2
    );
    ctx.lineTo(x + width, y + height - borderRadius);
    ctx.lineTo(x + width - borderRadius, y + height);
    ctx.arc(
      x + width - borderRadius,
      y + height - borderRadius,
      borderRadius,
      0,
      Math.PI * 0.5
    );
    ctx.lineTo(x + borderRadius, y + height);
    ctx.lineTo(x, y + height - borderRadius);
    ctx.arc(
      x + borderRadius,
      y + height - borderRadius,
      borderRadius,
      Math.PI * 0.5,
      Math.PI
    );
    ctx.lineTo(x, y + borderRadius);
    ctx.lineTo(x + borderRadius, y);
    ctx.fill();
    ctx.closePath();
    ctx.clip(); // 确定裁剪区域
    next();
  }

  custom(action : (ctx : UniApp.CanvasContext) => void) : Promise<void> {
    return new Promise((resolve) => {
      const { ctx } = this;
      ctx.save();
      action(ctx);
      ctx.restore();
      this.ctx.draw(true, resolve);
    });
  }

  getImage() : Promise<string> {
    this.ctx.fillStyle = "#fff";
    return new Promise((resolve, reject) => {
      this.ctx.draw(true, () => {
        uni.canvasToTempFilePath({
          x: 0,
          y: 0,
          canvasId: this.canvasId,
          success: (res) => {
            console.log('Taro.canvasToTempFilePath success res:', res)
            resolve(res.tempFilePath);
          },
          fail: (err) => {
            console.error('Taro.canvasToTempFilePath fail err:', err)
            reject(err)
          },
        });
      });
    });
  }
}
